﻿using System;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using Microsoft.Windows.Design.PropertyEditing;

namespace DenisVuyka.Controls.PropertyGrid
{
  using Metadata;
  using ValueEditors;

  public class PropertyItem : PropertyEntry, IDisposable
  {
    #region Fields
    private readonly object component;
    private readonly PropertyDescriptor descriptor;
    private readonly TypeConverter converter;
    private static ArrayList emptyList = new ArrayList(0);
    private AttributesContainer metadata;
    #endregion

    #region ctor/init

    /// <summary>
    /// Initializes a new instance of the <see cref="PropertyItem"/> class.
    /// </summary>
    /// <param name="component">The component.</param>
    /// <param name="descriptor">The descriptor.</param>
    public PropertyItem(object component, PropertyDescriptor descriptor)
    {
      if (component == null) throw new ArgumentNullException("component");
      if (descriptor == null) throw new ArgumentNullException("descriptor");

      this.component = component;
      this.descriptor = descriptor;

      if (PropertyHelper.StringConverterMembers.Contains(descriptor.Name) && descriptor.PropertyType.IsAssignableFrom(typeof(object)))
        this.converter = new StringConverter();
      else
        this.converter = descriptor.Converter;

      InitializeMetadata();
      this.descriptor.AddValueChanged(component, ComponentValueChanged);
    }

    private void InitializeMetadata()
    {
      this.metadata = (this.Attributes != null) ? new AttributesContainer(descriptor.Attributes) : null;
      OnPropertyChanged("Metadata");
    }

    private void ComponentValueChanged(object sender, EventArgs e)
    {
      OnPropertyChanged("PropertyValue");
    }

    #endregion

    #region Events

    public event Action<PropertyItem, object, object> ValueChanged;

    private void OnValueChanged(object oldValue, object newValue)
    {
      Action<PropertyItem, object, object> handler = ValueChanged;
      if (handler != null)
        handler(this, oldValue, newValue);
    }

    #endregion

    #region PropertyEntry implementation

    /// <summary>
    /// Gets the display name for the property.
    /// </summary>
    /// <value></value>
    /// <returns>
    /// The display name for the property.
    /// </returns>
    public override string DisplayName
    {
      get { return descriptor.DisplayName; }
    }

    /// <summary>
    /// Gets the name of the category that this property resides in.
    /// </summary>
    /// <value></value>
    /// <returns>
    /// The name of the category that this property resides in.
    /// </returns>
    public override string CategoryName
    {
      get { return descriptor.Category; }
    }

    /// <summary>
    /// Used by the host infrastructure to create a new host-specific <see cref="T:Microsoft.Windows.Design.PropertyEditing.PropertyValue"/> instance.
    /// </summary>
    /// <returns>
    /// A new <see cref="T:Microsoft.Windows.Design.PropertyEditing.PropertyValue"/> instance.
    /// </returns>
    protected override PropertyValue CreatePropertyValueInstance()
    {
      return new PropertyItemValue(this);
    }

    /// <summary>
    /// Gets the description of the encapsulated property.
    /// </summary>
    /// <value></value>
    /// <returns>
    /// The description of the encapsulated property.
    /// </returns>
    public override string Description
    {
      get { return descriptor.Description; }
    }

    /// <summary>
    /// Gets a value indicating whether the encapsulated property is an advanced property.
    /// </summary>
    /// <value></value>
    /// <returns>true if the encapsulated property is an advanced property; otherwise, false.
    /// </returns>
    public override bool IsAdvanced
    {
      get 
      {
        EditorBrowsableAttribute browsable = (EditorBrowsableAttribute)this.Attributes[typeof(EditorBrowsableAttribute)];
        return browsable != null && browsable.State == EditorBrowsableState.Advanced;
      }
    }

    private bool? customReadOnly;

    /// <summary>
    /// Gets a value indicating whether the encapsulated property is read-only.
    /// </summary>
    /// <value></value>
    /// <returns>true if the encapsulated property is read-only; otherwise, false.
    /// </returns>
    public override bool IsReadOnly
    {
      get 
      {
        if (customReadOnly.HasValue)
          return customReadOnly.Value;

        return descriptor.IsReadOnly; 
      }      
    }

    /// <summary>
    /// Gets the name of the encapsulated property.
    /// </summary>
    /// <value></value>
    /// <returns>
    /// The name of the encapsulated property.
    /// </returns>
    public override string PropertyName
    {
      get { return descriptor.Name; }
    }

    /// <summary>
    /// Gets the type of the encapsulated property.
    /// </summary>
    /// <value></value>
    /// <returns>
    /// The type of the encapsulated property.
    /// </returns>
    public override Type PropertyType
    {
      get { return descriptor.PropertyType; }
    }

    private PropertyValueEditor editor;

    /// <summary>
    /// Gets the <see cref="T:Microsoft.Windows.Design.PropertyEditing.PropertyValueEditor"/> that will be used to edit this <see cref="T:Microsoft.Windows.Design.PropertyEditing.PropertyEntry"/>.
    /// </summary>
    /// <value></value>
    /// <returns>
    /// The <see cref="T:Microsoft.Windows.Design.PropertyEditing.PropertyValueEditor"/> that will be used to edit this <see cref="T:Microsoft.Windows.Design.PropertyEditing.PropertyEntry"/>.
    /// </returns>
    public override PropertyValueEditor PropertyValueEditor
    {
      get
      {
        if (editor == null) editor = ValueEditorSelector.GetEditor(this);
        return editor;
      }
    }

    /// <summary>
    /// Gets the standard values that the encapsulated property supports.
    /// </summary>
    /// <value></value>
    /// <returns>
    /// A <see cref="T:System.Collections.ICollection"/> of standard values that the encapsulated property supports.
    /// </returns>
    public override ICollection StandardValues
    {
      get
      {
        if (this.Converter.GetStandardValuesSupported())
          return this.Converter.GetStandardValues();

        return emptyList;
      }
    }
    #endregion

    #region Properties

    /// <summary>
    /// Gets the component.
    /// </summary>
    /// <value>The component.</value>
    public object Component
    {
      get { return component; }
    }

    /// <summary>
    /// Gets the tool tip.
    /// </summary>
    /// <value>The tool tip.</value>
    public object ToolTip
    {      
      get
      {        
        DescriptionAttribute description = (DescriptionAttribute)Attributes[typeof(DescriptionAttribute)];
        return (description != null && !string.IsNullOrEmpty(description.Description))
          ? description.Description
          : DisplayName;
      }
    }

    /// <summary>
    /// Gets the attributes.
    /// </summary>
    /// <value>The attributes.</value>
    public AttributeCollection Attributes
    {
      get { return descriptor.Attributes; }
    }

    /// <summary>
    /// Gets the metadata.
    /// </summary>
    /// <value>The metadata.</value>
    public AttributesContainer Metadata
    {
      get { return metadata; }
    }

    /// <summary>
    /// Gets the converter.
    /// </summary>
    /// <value>The converter.</value>
    public TypeConverter Converter
    {
      get { return this.converter; }
    }

    /// <summary>
    /// Gets a value indicating whether this instance can clear value.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this instance can clear value; otherwise, <c>false</c>.
    /// </value>
    public bool CanClearValue
    {
      get { return descriptor.CanResetValue(component); }
    }

    // TODO: support this (UI should also react on it)
    /// <summary>
    /// Gets a value indicating whether this instance is default value.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this instance is default value; otherwise, <c>false</c>.
    /// </value>
    public bool IsDefaultValue
    {
      get { throw new NotImplementedException(); }
    }

    // TODO: investigate original purpose
    /// <summary>
    /// Gets a value indicating whether this instance is mixed value.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this instance is mixed value; otherwise, <c>false</c>.
    /// </value>
    public bool IsMixedValue
    {
      get { return false; }
    }

    /// <summary>
    /// Gets a value indicating whether this instance is collection.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this instance is collection; otherwise, <c>false</c>.
    /// </value>
    public bool IsCollection
    {
      get { return typeof(IList).IsAssignableFrom(this.PropertyType); }
    }        
    #endregion

    #region Methods
    public void ClearValue()
    {
      if (this.CanClearValue)
      {
        object oldValue = GetValue();
        descriptor.ResetValue(component);
        OnValueChanged(oldValue, GetValue());
        OnPropertyChanged("PropertyValue");
      }
    }

    public object GetValue()
    {
      object value;
      if (this.descriptor == null) return null;

      object target = GetViaCustomTypeDescriptor(component, descriptor);
      value = this.descriptor.GetValue(target);
      if (value == null) return null;
      return value;
    }

    private void SetValueCore(object value)
    {
      if (this.descriptor == null) return;

      // Check whether underlying dependency property passes validation
      if (!IsValidDependencyPropertyValue(this.descriptor, value))
      {
        OnPropertyChanged("PropertyValue");
        return;
      }
      
      object target = GetViaCustomTypeDescriptor(component, descriptor);

      if (target != null)
        this.descriptor.SetValue(target, value);
    }

    public void SetValue(object value)
    {
      // Check whether the property is not readonly
      if (IsReadOnly) return;
            
      object oldValue = GetValue();
      try
      {
        if (value != null && value.Equals(oldValue)) return;

        if (this.PropertyType == typeof(object) ||
          value == null && this.PropertyType.IsClass ||
          value != null && this.PropertyType.IsAssignableFrom(value.GetType()))
        {
          SetValueCore(value);
        }
        else
        {
          object convertedValue = this.Converter.ConvertFrom(value);
          SetValueCore(convertedValue);
        }
        OnValueChanged(oldValue, GetValue());
      }
      catch
      {
      }
      OnPropertyChanged("PropertyValue");
    }
    #endregion

    #region IDisposable Members

    private bool _disposed;

    /// <summary>
    /// Gets a value indicating whether this <see cref="PropertyItem"/> is disposed.
    /// </summary>
    /// <value><c>true</c> if disposed; otherwise, <c>false</c>.</value>
    protected bool Disposed
    {
      get { return _disposed; }
    }

    /// <summary>
    /// Releases unmanaged and - optionally - managed resources
    /// </summary>
    /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
    protected virtual void Dispose(bool disposing)
    {
      if (!Disposed)
      {
        _disposed = true;

        if (disposing)
          descriptor.RemoveValueChanged(component, ComponentValueChanged);
      }
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
      Dispose(true);
      GC.SuppressFinalize(this);
    }


    /// <summary>
    /// Releases unmanaged resources and performs other cleanup operations before the
    /// <see cref="PropertyItem"/> is reclaimed by garbage collection.
    /// </summary>
    ~PropertyItem()
    {
      Dispose(false);
    }

    #endregion

    #region Public helpers

    public void SetCustomReadOnlyState(bool? value)
    {
      this.customReadOnly = value;
      OnPropertyChanged("IsReadOnly");      
    }

    public T GetAttribute<T>() where T : Attribute
    {
      return this.Attributes[typeof(T)] as T;      
    }

    #endregion

    #region Private helpers
    private static object GetViaCustomTypeDescriptor(object obj, PropertyDescriptor descriptor)
    {
      if (obj is ICustomTypeDescriptor)
        return ((ICustomTypeDescriptor)obj).GetPropertyOwner(descriptor);

      return obj;
    }

    private static bool IsValidDependencyPropertyValue(PropertyDescriptor descriptor, object value)
    {
      bool result = true;

      DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(descriptor);
      if (dpd != null)
      {
        if (dpd.DependencyProperty != null)
          result = dpd.DependencyProperty.IsValidValue(value);
      }

      return result;
    }
    #endregion
  }
}
